﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Text.RegularExpressions;

namespace RTF2XML
{
    public class Group : GroupItem
    {
        #region Private variables
        /// <summary>
        /// Full, unparsed text for everything within the group
        /// </summary>
        private string sRawText = "";

        /// <summary>
        /// Holds the instance of the control class to be used for parsing out control types
        /// </summary>
        private Control control;

        /// <summary>
        /// The parent group of this group
        /// </summary>
        private Group parent;

        /// <summary>
        /// Holds a list of all controls, groups, and text that are parsed out of the group
        /// </summary>
        private List<GroupItem> lstGroupItemControls = new List<GroupItem>();

        /// <summary>
        /// Holds a list of all controls that aren't able to be matched
        /// </summary>
        private List<string> lstUnmatchedControls = new List<string>();
        #endregion

        #region Public Properties
        /// <summary>
        /// Gest a list of all controls, groups, and text that are within the group
        /// </summary>
        public List<GroupItem> GroupItemControls
        {
            get
            {
                return this.lstGroupItemControls;
            }
        }

        /// <summary>
        /// Gets a list of all controls within the current group, and all nested groups, that weren't able to be matched to a control
        /// </summary>
        public List<string> UnmatchedControls
        {
            get
            {
                //Get the list of all unmatched controls within the current group
                List<string> lstAllUnmatchedControls = this.lstUnmatchedControls;

                //For each nested group within the current group
                foreach (Group nestedGroup in this.lstGroupItemControls.Where(i => i.GetType() == typeof(Group)).ToList())
                {
                    //Get the list of unmatched controls from the nested group and concat that to the list of unmatched controls in the current group
                    lstAllUnmatchedControls = lstAllUnmatchedControls.Concat(nestedGroup.UnmatchedControls).ToList();
                }

                //Return the list of all unmatched controls within the current group and all nested groups
                return lstAllUnmatchedControls;
            }
        }

        /// <summary>
        /// Gets if the group has any destination controls within it
        /// </summary>
        public bool HasDestinationControl
        {
            get
            {
                //If there are any controls in the group that are marked as destination controls
                if (this.lstGroupItemControls.Where(i => i.IsDestination).ToList().Count > 0)
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
        }

        /// <summary>
        /// Gets if any parent group has any destination controls within them
        /// </summary>
        public bool ParentHasDestinationControl
        {
            get
            {
                //If the group has a parent and the parent has a destination control
                //or any of the parent's parent's have destination controls (a recursive call to see if any parent control has a destination control)
                if (this.parent != null && (this.parent.HasDestinationControl || this.parent.ParentHasDestinationControl))
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
        }
        #endregion

        #region Constructors
        /// <summary>
        /// Creates an instance of the Group class which will use
        /// the static GetTypeOfControl method in the Control class to parse controls
        /// </summary>
        /// <param name="_sRawText">Unparsed text inside the group</param>
        /// <param name="_parent">Parent group</param>
        public Group(string _sRawText, Group _parent)
        {
            //Uncomment this line if you want to keep the raw input text (for debugging)
            //this.sRawText = _sRawText;

            this.parent = _parent;

            //Begins the recursive parsing of every control, group, and text within the input text
            this.ParseControls(_sRawText);
        }

        /// <summary>
        /// Creates a new instance of the Group class that uses the
        /// instance of the Control class to parse the input text
        /// </summary>
        /// <param name="_sRawText">Unparsed text inside the group</param>
        /// <param name="_control">Control class to use for parsing into text</param>
        /// /// <param name="parent">Parent group</param>
        public Group(string _sRawText, Control _control, Group _parent)
        {
            //Uncomment this line if you want to keep the raw input text (for debugging)
            //this.sRawText = _sRawText;

            this.control = _control;
            this.parent = _parent;

            //Begins the recursive parsing of every control, group, and text within the input text
            this.ParseControls(_sRawText);
        }
        #endregion

        #region Control Parsing
        /// <summary>
        /// Parses the raw text of the group into controls, plain text, and groups
        /// </summary>
        private void ParseControls(string _sRawText)
        {
            #region Initialization
            //Mkae sure the list of group items is new and clear
            this.lstGroupItemControls = new List<GroupItem>();

            //Stack of characters used to parse out the groups and individual controls
            Stack<char> stkGroup = new Stack<char>();
            //Boolean to tell if you are within a nested group (where you don't yet want to parse the individual controls)
            bool bIsInGroup = false;
            //Integer to count how deeply nested you are
            int iGroupNesting = 0;
            #endregion

            //For each character in the raw text
            //Replace new line characters with spaces so that they don't throw off the parsing (if you take it out you'll see what I mean)
            foreach (char c in _sRawText.Replace("\r\n", " "))
            {
                #region Not in a nested group
                //If you aren't withing a nested group
                if (!bIsInGroup)
                {
                    //If at a space then count as the end of a group item (i.e. control)
                    if (c == ' ')
                    {
                        //Get out the group item
                        string sGroupItem = "";
                        while (stkGroup.Count > 0)
                        {
                            char chr = stkGroup.Pop();
                            if (chr == ' ')
                            {
                                break;
                            }

                            sGroupItem = chr + sGroupItem;
                        }

                        //Parse the new item into a control or plain text for the group
                        this.ParseIntoItem(sGroupItem);
                    }
                    //If the start of a group
                    else if (c == '{')
                    {
                        //Get anything before the new group
                        string sGroupItem = "";
                        while (stkGroup.Count > 0)
                        {
                            char chr = stkGroup.Pop();
                            if (chr == ' ')
                            {
                                break;
                            }

                            sGroupItem = chr + sGroupItem;
                        }
                        if (sGroupItem != "")
                        {
                            //Parse the new item into a control or plain text for the group
                            this.ParseIntoItem(sGroupItem);
                        }

                        //Start the new group
                        bIsInGroup = true;
                        stkGroup.Push(c);
                    }
                    else
                    {
                        //If anything else then just push the character and continue
                        stkGroup.Push(c);
                    }
                }
                #endregion
                #region In a nested group
                //If within a nested group
                else
                {
                    //If at the start of a nested group
                    if (c == '{')
                    {
                        iGroupNesting++;
                        stkGroup.Push(c);
                    }
                    //If the end of the group
                    else if (c == '}')
                    {
                        //If at the end of the outer-most group
                        if (iGroupNesting == 0)
                        {
                            //Get out the entire group
                            string sGroup = "";
                            while (stkGroup.Count > 1)
                            {
                                char chr = stkGroup.Pop();
                                sGroup = chr + sGroup;
                            }
                            //To get rid of the beginning {
                            stkGroup.Pop();

                            //Create a nested group
                            if (this.control == null)
                            {
                                this.lstGroupItemControls.Add(new Group(sGroup, this));
                            }
                            else
                            {
                                this.lstGroupItemControls.Add(new Group(sGroup, this.control, this));
                            }

                            //No longer within a nested group
                            bIsInGroup = false;
                        }
                        //If at the end of a nested group
                        else
                        {
                            //Take away one from the nested count
                            iGroupNesting--;

                            //Continue to push characters onto the stack
                            stkGroup.Push(c);
                        }
                    }
                    //Else continue to add to the group
                    else
                    {
                        stkGroup.Push(c);
                    }
                }
                #endregion
            }

            #region Parse anything left at the end of the group
            //If there are still characters left in the stack remove them and parse
            if (!bIsInGroup && stkGroup.Count > 0)
            {
                while (stkGroup.Count > 0)
                {
                    //Get out the group item
                    string sGroupItem = "";
                    while (stkGroup.Count > 0)
                    {
                        char chr = stkGroup.Pop();
                        sGroupItem = chr + sGroupItem;
                    }

                    //Parse the new item into a control or plain text for the group
                    this.ParseIntoItem(sGroupItem);
                }
            }
            #endregion
            //If group did not end then it's not properly formatted
            else if (stkGroup.Count > 0)
            {
                throw new Exception("Groups must have ending '}'");
            }
        }

        /// <summary>
        /// Parses an individual item into either a particular control, plain text, or is disregarded
        /// </summary>
        /// <param name="sItem"></param>
        private void ParseIntoItem(string sItem)
        {
            //Create a regular expression to get out text that is in the formation of a control word (text with a single \ in front of it)
            Regex regex = new Regex(@"(?<!\\)\\(\w+)");

            //Get all the matches to the regular expression in the input text
            MatchCollection matches = regex.Matches(sItem);

            #region Parse Control
            //If the item looks to be a control
            if (matches.Count > 0)
            {
                //For each regular expression match
                foreach (Match m in matches)
                {
                    Control ctrl = null;

                    //If you don't have an instance of the Control class to use
                    if (this.control == null)
                    {
                        //Use static method to get the control type
                        ctrl = Control.GetTypeOfControl(m.Value.Replace("\\", ""));
                    }
                    else
                    {
                        //Use method to get control type
                        ctrl = this.control.GetControlType(m.Value.Replace("\\", ""));
                    }

                    //If you found a control that matches the text the add that control
                    if (ctrl != null)
                    {
                        this.lstGroupItemControls.Add(ctrl);
                    }
                    //If no matching control was found then add text to a list of unmatched controls
                    else
                    {
                        //This doesn't account for numbered controls, it should strip off the number at the end if there is one.
                        this.lstUnmatchedControls.Add(m.Value.Replace("\\", ""));
                    }
                }
            }
            #endregion
            //If the item is not in the format of a control it is either plain text or destination text
            else
            {
                #region Parse Text
                #region Parse Destination Text
                //If the current group or a parent group has a destination control
                if (this.HasDestinationControl || this.ParentHasDestinationControl)
                {
                    //Check to see if the previous item was a destination text
                    if (this.lstGroupItemControls.Count > 0 && this.lstGroupItemControls[this.lstGroupItemControls.Count - 1].GetType() == typeof(DestinationText))
                    {
                        //Concat the text together into one control
                        ((DestinationText)this.lstGroupItemControls[this.lstGroupItemControls.Count - 1]).sText += " " + sItem;
                    }
                    else
                    {
                        //Text is destination text
                        DestinationText text = new DestinationText(sItem);
                        //Add destination text to group item controls
                        this.lstGroupItemControls.Add(text);
                    }
                }
                #endregion
                #region Parse Plain Text
                //If not destination text then it's plain text
                else
                {
                    //Check to see if the previous item was a destination text
                    if (this.lstGroupItemControls.Count > 0 && this.lstGroupItemControls[this.lstGroupItemControls.Count - 1].GetType() == typeof(PlainText))
                    {
                        //Concat the text together into on control
                        ((PlainText)this.lstGroupItemControls[this.lstGroupItemControls.Count - 1]).sText += " " + sItem;
                    }
                    else
                    {
                        //Create a new plain text control
                        PlainText text = new PlainText(sItem);
                        //Add plain text control the the group item controls
                        this.lstGroupItemControls.Add(text);
                    }
                }
                #endregion
                #endregion
            }
        }
        #endregion

        #region XML
        /// <summary>
        /// Creates a group node and all children of the group
        /// </summary>
        /// <param name="xmlParentNode">Parent node</param>
        public override void AddXML(ref XmlElement xmlParentNode)
        {
            #region Create group node
            int iSeqNum = 0;

            if (xmlParentNode.HasChildNodes)
            {
                //Get the sequence number of the last child in the parent
                string seqNum = xmlParentNode.LastChild.Attributes["seq"].Value;
                if (int.TryParse(seqNum, out iSeqNum))
                {
                    //Increment the sequence number
                    iSeqNum++;
                }
                else
                {
                    throw new Exception("Sequence number is not valid");
                }
            }

            //Create a new group element
            XmlElement child = xmlParentNode.OwnerDocument.CreateElement("Group");
            //Add the sequence attribute to the group element
            child.SetAttribute("seq", iSeqNum.ToString());
            #endregion

            #region Create the children elements of the group with the group item controls
            //For each group item control
            foreach (GroupItem childItem in this.lstGroupItemControls)
            {
                //Add the xml of the child element to the group element
                childItem.AddXML(ref child);
            }
            #endregion

            //Add the group element to the parent node
            xmlParentNode.AppendChild(child);
        }

        /// <summary>
        /// Creates a group node and all children of the group
        /// </summary>
        /// <param name="xmlDoc">Document to add the group xml to</param>
        public override void AddXML(ref XmlDocument xmlDoc)
        {
            #region Create group node
            int iSeqNum = 0;

            if (xmlDoc.HasChildNodes)
            {
                //Get the sequence number of the last child in the document (most likely none)
                string seqNum = xmlDoc.LastChild.Attributes["seq"].Value;
                if (int.TryParse(seqNum, out iSeqNum))
                {
                    //Increment the sequence number
                    iSeqNum++;
                }
                else
                {
                    throw new Exception("Sequence number is not valid");
                }
            }

            //Create the new group element
            XmlElement child = xmlDoc.CreateElement("Group");
            //Add the sequence attribute to the group element
            child.SetAttribute("seq", iSeqNum.ToString());
            #endregion

            #region Create the children elements of the group with the group item controls
            //For each group item control
            foreach (GroupItem childItem in this.lstGroupItemControls)
            {
                //Add the xml of the child element to the group element
                childItem.AddXML(ref child);
            }
            #endregion

            //Add the group element to the document
            xmlDoc.AppendChild(child);
        }
        #endregion
    }
}
